import { spawn } from 'child_process';
import { copyFile, unlink } from 'fs/promises';
import config from './config.js';
import TaskPool from './TaskPool.js';
import BiliApi from './api/BiliApi.js';
import ZimuApi from './api/ZimuApi.js';
import PushApi from './api/PushApi.js';

export default class BililiveService {

    constructor() {
        this.infoTaskPool = new TaskPool();
        this.infoTaskPool.run();

        this.diskTaskPool = new TaskPool();
        this.diskTaskPool.run();

        this.roomMap = new Map();

        this.localSet = new Set();
        for (let i = 0; i < config.rec.locals.length; ++i) {
            this.localSet.add(config.rec.locals[i]);
            console.log(`local:${config.rec.locals[i]}`);
        }
    }

    addLocal = async (ctx) => {
        const body = ctx.request.body;
        ctx.logger.info(body);

        const locals = body.locals;
        for (let i = 0; i < locals.length; ++i) {
            const item = locals[i];
            this.localSet.add(item);
            ctx.logger.info(`add local ${item}`);
        }
    }

    delLocal = async (ctx) => {
        const id = parseInt(ctx.params.id);
        this.localSet.delete(id);
        ctx.logger.info(`del local ${id}`);
    }

    getLocal = async (ctx) => {
        return Array.from(this.localSet);
    }

    webhook = async (ctx) => {
        const body = ctx.request.body;
        ctx.logger.info(body);

        const type = body.EventType;
        if (type === 'FileOpening') {
            ctx.logger.info('录制开始webhook'); 
            const filename = body.EventData.RelativePath.split('/')[2];
            const datetime = `${filename.substring(0, 4)}-${filename.substring(4, 6)}-${filename.substring(6, 8)} ${filename.substring(9, 11)}:${filename.substring(11, 13)}:${filename.substring(13, 15)}`;
            const roomId = body.EventData.RoomId;
            const name = body.EventData.Name;
            const title = body.EventData.Title.replaceAll('*', '_').replaceAll('?', '_').replaceAll('\\', '_'); // 针对某些标题中含有*和?的情况，为了兼容windows系统文件，将*和?换成_

            this.infoTaskPool.push(async () => {
                // 从bilibili获取到直播间基础信息
                const roomInfo = await BiliApi.getRoomInfo(roomId);
                const uid = roomInfo.uid;
                const cover = roomInfo.user_cover.substring(8); // 去掉https://

                PushApi.push('录制开始', `${name},${title}`);

                const clip = {
                    uid:        uid,
                    title:      title,
                    datetime:   datetime,
                    cover:      cover,
                    type:       4
                };

                // 重试n次
                for (let i = 0; i < config.web.retry; ++i) {
                    try {
                        const newClip = await ZimuApi.insertClip(clip);
                        this.roomMap.set(roomId, newClip);
                        ctx.logger.info(`创建新clip:`);
                        ctx.logger.info(this.roomMap.get(roomId));
                        break;
                    } catch (ex) {
                        ctx.logger.error(ex);
                    }
                    await new Promise((res, rej) => {
                        setTimeout(() => { res(); }, 3000);
                    });
                }
            });
        } else if (type === 'FileClosed') {
            ctx.logger.info('录制结束webhook');
            const roomId = body.EventData.RoomId;
            const name = body.EventData.Name;
            const title = body.EventData.Title;
            const duration = body.EventData.Duration;

            this.infoTaskPool.push(async () => {
                const clip = this.roomMap.get(roomId);
                if (!clip) {
                    ctx.logger.error(`房间(${roomId})找不到clip`);
                    return;
                }
                this.roomMap.set(roomId, null);

                const message = `${name},${title},${duration}s`;
                if (duration < config.rec.minInterval) {
                    // 如果录制时间过短，则删掉该clip在字幕库中的信息，但是录播文件不删除
                    ctx.logger.info(`时间过短:${message},不得低于${config.rec.minInterval}s`);
                    PushApi.push('时间过短', message);

                    // 重试10次
                    for (let i = 0; i < config.web.retry; ++i) {
                        try {
                            await ZimuApi.deleteClip(clip.id);
                            break;
                        } catch (ex) {
                            ctx.logger.error(ex);
                        }
                        await new Promise((res, rej) => {
                            setTimeout(() => { res(); }, 3000);
                        });
                    }
                } else {
                    PushApi.push('录制结束', message);
                    // 本地源的录播需要改变状态到本地源，B站源的录播需要改变状态到已下播
                    let newType = -1;
                    if (this.localSet.has(clip.authorId)) {
                        newType = 3;
                    } else {
                        newType = 5;
                    }
                    for (let i = 0; i < config.web.retry; ++i) { // 重试10次
                        try {
                            const newClip = await ZimuApi.updateClip(clip.id, { type: newType });
                            ctx.logger.info('clip更新后:');
                            ctx.logger.info(newClip);
                            break;
                        } catch (ex) {
                            ctx.logger.error(ex);
                        }
                        await new Promise((res, rej) => {
                            setTimeout(() => { res(); }, 3000);
                        });
                    }
                }

                // 接下来的一系列转码和文件转移操作，均只涉及到本地源，B站源的录播可以直接退出
                if (!this.localSet.has(clip.authorId)) return {};

                // 生成flv,mp4,xml,txt的源和目的文件路径
                const flvname = body.EventData.RelativePath.split('/')[2];
                const xmlname = flvname.replace('.flv', '.xml');
                const mp4name = flvname.replace('.flv', '.mp4');
                // const m4aname = flvname.replace('.flv', '.m4a');
                const m4aname = `${clip.id}.m4a`;
                const txtname = flvname.replace('.flv', '.txt');

                const liveflvpath = `${config.rec.live}/${body.EventData.RelativePath}`;
                const livexmlpath = liveflvpath.replace('.flv', '.xml');
                const livetxtpath = liveflvpath.replace('.flv', '.txt');

                const rootflvpath = `${config.rec.root}/${body.EventData.RelativePath}`;
                const rootxmlpath = rootflvpath.replace('.flv', '.xml');
                const rootmp4path = rootflvpath.replace('.flv', '.mp4');

                const dstm4apath = `${config.rec.m4a}/${m4aname}`;
                const dstmp4path = `${config.rec.mp4}/${mp4name}`;
                const dstxmlpath = `${config.rec.mp4}/${xmlname}`;
                const dstflvpath = `${config.rec.mp4}/${flvname}`;
                ctx.logger.info({liveflvpath, livexmlpath, dstm4apath, dstmp4path});

                ctx.logger.info('准备处理数据转换和迁移');
                this.diskTaskPool.push(async () => {
                    try {
                        // 复制xml到mp4待转区
                        await copyFile(livexmlpath, dstxmlpath);
                        ctx.logger.info(`复制${livexmlpath}到${dstxmlpath}结束`);              
                        
                        // 复制flv到mp4待转区
                        await copyFile(liveflvpath, dstflvpath);
                        ctx.logger.info(`复制${liveflvpath}到${dstflvpath}结束`); 

                        ctx.logger.info('开始flv转m4a');
                        await this._toM4A(ctx, dstflvpath, dstm4apath);

                        ctx.logger.info('开始flv转mp4');
                        await this._toMP4(ctx, dstflvpath, dstmp4path);
                        
                        // 时间长度够了才放root区
                        if (duration >= config.rec.minInterval) {
                            // 复制mp4到root区
                            await copyFile(dstmp4path, rootmp4path);
                            ctx.logger.info(`复制${dstmp4path}到${rootmp4path}结束`);

                            // 复制xml到root区
                            await copyFile(dstxmlpath, rootxmlpath);
                            ctx.logger.info(`复制${dstxmlpath}到${rootxmlpath}结束`);
                        }

                        await unlink(livexmlpath);
                        ctx.logger.info(`删除${livexmlpath}结束`);
                        await unlink(liveflvpath);
                        ctx.logger.info(`删除${liveflvpath}结束`);
                        await unlink(dstflvpath);
                        ctx.logger.info(`删除${dstflvpath}结束`);
                        try {
                            await unlink(livetxtpath);
                        } catch(ex) {}
                    } catch (ex) {
                        ctx.logger.error(ex);
                    }
                });
            });
        }
        return {};
    }

    _toM4A = async (ctx, flvpath, m4apath) => {
        return new Promise((res, rej) => {
            const cmd = [
                '-i', flvpath,
                '-vn',
                '-codec', 'copy',
                m4apath
            ];
            let p = spawn('ffmpeg', cmd, {windowsHide:true});
            p.stdout.on('data', (data) => {
                ctx.logger.info('stdout: ' + data.toString());
            });
            p.stderr.on('data', (data) => {
                ctx.logger.info('stderr: ' + data.toString());
            });
            p.on('close', (code) => {
                ctx.logger.info(`flv转m4a结束,ffmpeg退出:code:${code}`);
                res();
            });
            p.on('error', (error) => {
                rej(error);
            });
        }).catch(ex => {
            ctx.logger.error(ex);
            PushApi.push('flv转m4a异常', `${ex}`);
        });
    }

    _toMP4 = async (ctx, flvpath, mp4path) => {
        return new Promise((res, rej) => {
            const cmd = [
                '-i', flvpath,
                '-c', 'copy',
                '-movflags', 'faststart',
                mp4path
            ];
            let p = spawn('ffmpeg', cmd, {windowsHide:true});
            p.stdout.on('data', (data) => {
                ctx.logger.info('stdout: ' + data.toString());
            });
            p.stderr.on('data', (data) => {
                ctx.logger.info('stderr: ' + data.toString());
            });
            p.on('close', (code) => {
                ctx.logger.info(`flv转mp4结束,ffmpeg退出:code:${code}`);
                res();
            });
            p.on('error', (error) => {
                rej(error);
            });
        }).catch(ex => {
            ctx.logger.error(ex);
            PushApi.push('flv转mp4异常', `${ex}`);
        });
    }
}